package owg.util.opengl;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;

import com.jogamp.opengl.GL2;

import owg.util.Calc;


/**
 * Class for holding an armature animation loaded from a bvh file 
 */
public class Armature
	{
	private static HashMap<String,Armature> armatures = new HashMap<String,Armature>();
	/**This method looks for the armature with the given file name.
	 * If it is already in memory, the first instance that was loaded is returned.
	 * Otherwise, it is loaded from disk.
	 * If multiple instances of the armature are required, use softCopy on the returned instance.
	 * Passing null as fileName will cause null to be returned.*/
	public static Armature get(String fileName)
		{
		if(fileName == null)
			return null;
		if(armatures.containsKey(fileName))
			return armatures.get(fileName);
		else
			{
			Armature a = new Armature(fileName);
			armatures.put(fileName, a);
			return a;
			}
		}
	
	HashMap<String,Bone> boneMap;
	public Bone[] bones;
	public int frames;
	public float frameTime;
	/**Provides a hint as to whether this instance is being used at the moment.
	 * Strive to set this to false when the user is destroyed.
	 * The value of this field is managed entirely by users of the class, 
	 * it is not used internally, and initially false.*/
	public boolean isInUse;
	
	private Armature(String filename)
		{
		isInUse=false;
		//Bones will be topologically sorted since it's a tree.
		Stack<Bone> parents = new Stack<Bone>();
		Queue<Bone> boneQ = new LinkedList<Bone>();
		boneMap = new HashMap<String,Bone>();
		Bone bone = null;
		int boneIndex=0;
		@SuppressWarnings("unused")
		int frameIndex=0;
		//Flag set to true when encountering "End Site"
		//The endsite information is not needed.
		boolean endSite = false;
		//Flag set when entering the frame data phase
		boolean frameData = false;
		
		File file = new File("res/armature/"+filename);
		FileReader fr = null;
		BufferedReader br = null;
		try//Setup readers
			{
			fr = new FileReader(file);
			br = new BufferedReader(fr);
			
			String line;
			while((line=br.readLine())!=null)
				{
				line = Mtllib.strip(line, 0);
				
				if (line.startsWith("ROOT") || line.startsWith("JOINT"))
					{
					line = line.substring(line.indexOf(' ')+1);
					//System.out.println(line+" parent to "+((bone==null)?"null":(bone.name)));
					
					
					parents.push(bone);
					
					bone = new Bone(this, bone, line, boneIndex);
					boneMap.put(line, bone);
					boneQ.offer(bone);
					boneIndex++;
					}
				else if (line.startsWith("CHANNELS"))
					{
					line = line.substring(line.indexOf(' ')+1);
					
					int numEndIndex = line.indexOf(' ');
					if(numEndIndex == -1)//If CHANNELS is 0, then there might not be a space following the number
						numEndIndex = line.length();
					
					String numStr = line.substring(0,numEndIndex);
					int channels = Integer.parseInt(numStr);
					
					if (channels > 0)
						bone.enableRotData();
					if (line.contains("Xposition"))
						bone.enablePosData();
					if (line.contains("Xscale"))
						bone.enableScaData();
					}
				else if (line.startsWith("}"))
					{
					if(!endSite)
						{
						bone = parents.pop();//Go back up a level
						}
					endSite=false;
					}
				else if (line.startsWith("End Site"))
					{
					endSite=true;
					}
				else if (line.startsWith("MOTION"))
					{
					frameData=true;
					}
				else if (line.startsWith("Frames: "))
					{
					frames=Integer.parseInt(line.substring(8));
					//Build array.
					bones = new Bone[boneIndex];
					for(int i=0; i<boneIndex; i++)
						{
						bones[i]=boneQ.poll();
						bones[i].findMirror();
						bones[i].setFrameNumber(frames);
						}
					}
				else if (line.startsWith("Frame Time: "))
					{
					frameTime=Float.parseFloat(line.substring(12));
					}
				else if (line.startsWith("OFFSET"))
					{
					line = line.substring(line.indexOf(' ')+1);
					if(endSite)
						bone.setEndSite(Calc.getFloatArray(line));
					else
						bone.setOffsetFromParent(Calc.getFloatArray(line));
					}
				else if (frameData)
					{
					float[] data = Calc.getFloatArray(line);
					int position=0;
					for(int i=0; i<boneIndex; i++)
						{
						bones[i].addData(data, position);
						position+=bones[i].getBlockSize();
						}
					frameIndex++;
					}
				}
			}
		catch (IOException e)//Print info for debugging if anything fails
			{
			System.err.println("Armature loader fail: "+file);
			e.printStackTrace();
			}
		catch (NumberFormatException e)
			{
			System.err.println("Armature loader fail: "+file);
			e.printStackTrace();
			}
		finally//Close readers if they were opened at all
			{
			try {br.close();
				fr.close();}
			catch (Exception e){}
			}
		}
	private Armature()
		{}
	/**Compute matrices and vectors for the given frame.
	 * @param interpolate 0 means the old deform is kept. 1 means the old deform is completely overwritten.*/
	public void computeFrame(int frame,float interpolate, boolean mirror)
		{
		if(frame < 0 || frame >= frames)
		throw new IllegalStateException("Frame index out of bounds: "+frame+" (0-"+frames+")");
		for (int i=0; i<bones.length; i++)
			{
			bones[i].compute(frame,interpolate,mirror,false);
			}
		}
	@Override
	public String toString()
		{
		String str = "Armature: ";
		for(int i=0; i<bones.length; i++)
			{
			str+=bones[i].toString();
			}
		str+=" [EOF]";
		return str;
		}
	/**Get a copy that uses the same data arrays*/
	public Armature softCopy()
		{
		Armature result = new Armature();
		result.frameTime = frameTime;
		result.frames = frames;
		result.boneMap = new HashMap<String,Bone>();
		result.bones = new Bone[bones.length];
		for(int i=0; i<bones.length; i++)
			{
			Bone b = new Bone(result,bones[i]);
			result.bones[i] = b;
			result.boneMap.put(bones[i].name, b);
			}
		for(int i=0; i<bones.length; i++)
			{
			if(bones[i].getParent()!=null)
				result.bones[i].setParent(result.getBone(bones[i].getParent().name));
			result.bones[i].findMirror();
			}
		result.isInUse=false;
		return result;
		}
	/**@return The bone in the Armature with the indicated name. 
	 * Will return null if the Armature does not contain a bone with this name.*/
	public Bone getBone(String boneName)
		{
		return boneMap.get(boneName);
		}
	/**Create a new bvh file containing the animation data from all source files.
	 * The files should definitely have identical bone structure.
	 * The only excuse to use this method is that blender's bvh exporter is unbearably slow.
	 * @return The frame numbers of the source bvhs*/
	public static int[] joinBVH(String output, String... sources)
		{
		try
			{
			int[] lengths = new int[sources.length];
			//Create output
			File out = new File("res/armature/"+output);
			PrintWriter bo = new PrintWriter(out);
			//data structure
			Queue<String> data = new LinkedList<String>();
			//
			ArrayList<String> boneStructure = new ArrayList<String>();
			//Total frame count
			int frames=0;
			int frameLength = -1;
			float frameTime = 0;
			//Read all source files
			for(int i=0; i<sources.length; i++){
				//Open source file
				File in = new File("res/armature/"+sources[i]);
				BufferedReader bi = new BufferedReader(new FileReader(in));
				//Scan until frame number
				String str;
				int ind = 0;
				while(!(str=bi.readLine()).startsWith("Frames: "))
					{
					//Write to out, only for first file
					if(i==0)
						{
						bo.println(str);
						boneStructure.add(str);
						}
					else
						{
						if(!str.equals(boneStructure.get(ind)))
							{
							System.err.println("Warning: "+sources[i]+" has different frame structure from "+sources[0]);
							System.err.println("    At line: "+ind+": Found "+str+", expected"+boneStructure.get(ind));
							}
						}
					ind++;
					}					
				//Add to frame count
				lengths[i] = Integer.parseInt(str.substring(8)); 
				frames+=lengths[i];
				//Read frame count
				str=bi.readLine();
				if(i==0)
					frameTime=Float.parseFloat(str.substring(12));
				//Read till end, put data in structure
				boolean first = true;
				while((str=bi.readLine()) != null)
					{
					if(first)
						{
						int fl = str.split(" ").length;
						if(i == 0)
							frameLength = fl;
						else if(fl != frameLength)
							System.err.println("Warning: "+sources[i]+" has different frame structure from "+sources[0]);
						first = false;
						}
					data.offer(str);
					}
				//Close source file
				bi.close();
			}
			//Write total frame count
			bo.println("Frames: "+frames);
			//Write frame time
			bo.println("Frame Time: "+frameTime);
			//Write all the data
			while(!data.isEmpty())
				{
				bo.println(data.poll());
				}
			//Close output file
			bo.close();
			return lengths;
			}
		catch (IOException e)
			{
			System.err.println("BVH join fail: " + output);
			e.printStackTrace();
			}
		return null;
		}
	/**Debug method. Renders the armature as a line list into GFX.gl*/
	public void render(EGL<?> glUtil)
		{
		ImmediateModeWrapper<?> gl = glUtil.immediateMode();
		gl.glColor3f(1, 0, 1);
		gl.glBegin(false, GL2.GL_LINES, 3, false, 0, 0);
		for(Bone b: bones)
			{
			if(b.getParent() != null)
				{
				gl.glVertex3fv(b.currentPosition,0);
				gl.glVertex3fv(b.getParent().currentPosition,0);
				}
			}
		gl.glEnd();
		}
	}
